<template>
  <div class="knowledge-graph-container">
    <div class="control-panel">
      <div class="filters">
        <div class="filter-group">
          <h3>实体类型过滤</h3>
          <div v-for="(checked, type) in nodeTypeFilters" :key="type" class="filter-item">
            <input type="checkbox" :id="type" v-model="nodeTypeFilters[type]" @change="applyFilters">
            <label :for="type" :style="{ backgroundColor: nodeColorMap[type] || '#ccc' }">{{ type }}</label>
          </div>
        </div>
        
        <div class="filter-group">
          <h3>关系类型过滤</h3>
          <div v-for="(checked, type) in linkTypeFilters" :key="type" class="filter-item">
            <input type="checkbox" :id="type" v-model="linkTypeFilters[type]" @change="applyFilters">
            <label :for="type">{{ type }}</label>
          </div>
        </div>
        
        <div class="filter-group">
          <h3>搜索</h3>
          <input type="text" v-model="searchTerm" placeholder="搜索节点..." @input="applyFilters">
        </div>
      </div>
      
      <div class="actions">
        <button @click="resetZoom" class="action-button">重置视图</button>
        <button @click="centerGraph" class="action-button">居中图谱</button>
        <button @click="downloadSVG" class="action-button">导出 SVG</button>
      </div>
      
      <div v-if="selectedNode" class="node-details">
        <h3>节点详情</h3>
        <div class="detail-row">
          <strong>类型:</strong> {{ selectedNode.type }}
        </div>
        <div class="detail-row">
          <strong>ID:</strong> {{ selectedNode.id }}
        </div>
        <div class="detail-row">
          <strong>名称:</strong> {{ selectedNode.name }}
        </div>
        <div v-if="selectedNode.value" class="detail-row">
          <strong>值:</strong> {{ selectedNode.value }}
        </div>
        <div v-if="selectedNode.confidence !== undefined" class="detail-row">
          <strong>可信度:</strong> {{ (selectedNode.confidence * 100).toFixed(2) }}%
        </div>
        <button @click="closeDetails" class="close-button">关闭</button>
      </div>
      
      <div v-if="selectedLink" class="link-details">
        <h3>关系详情</h3>
        <div class="detail-row">
          <strong>类型:</strong> {{ selectedLink.type }}
        </div>
        <div class="detail-row">
          <strong>源:</strong> {{ getNodeNameById(selectedLink.source) }}
        </div>
        <div class="detail-row">
          <strong>目标:</strong> {{ getNodeNameById(selectedLink.target) }}
        </div>
        <div v-if="selectedLink.confidence !== undefined" class="detail-row">
          <strong>可信度:</strong> {{ (selectedLink.confidence * 100).toFixed(2) }}%
        </div>
        <div v-if="selectedLink.created" class="detail-row">
          <strong>创建时间:</strong> {{ formatDate(selectedLink.created) }}
        </div>
        <button @click="closeDetails" class="close-button">关闭</button>
      </div>
    </div>
    
    <div class="graph-container">
      <div class="legend">
        <div v-for="(color, type) in nodeColorMap" :key="type" class="legend-item">
          <span class="legend-color" :style="{ backgroundColor: color }"></span>
          <span class="legend-label">{{ type }}</span>
        </div>
      </div>
      
      <div class="zoom-controls">
        <button @click="zoomIn" class="zoom-button">+</button>
        <button @click="zoomOut" class="zoom-button">-</button>
      </div>
      
      <svg ref="svg" width="100%" height="100%"></svg>
      
      <div v-if="loading" class="loading-overlay">
        <div class="spinner"></div>
        <div>加载知识图谱...</div>
      </div>
    </div>
  </div>
</template>

<script>
import * as d3 from 'd3';
import axios from 'axios';

export default {
  name: 'KnowledgeGraph',
  props: {
    apiUrl: {
      type: String,
      default: 'http://localhost:8000'
    },
    token: {
      type: String,
      required: true
    },
    apiEndpoint: {
      type: String,
      default: 'graph'  // 默认使用标准graph端点
    }
  },
  data() {
    return {
      loading: true,
      graphData: { nodes: [], links: [] },
      filteredData: { nodes: [], links: [] },
      simulation: null,
      svg: null,
      g: null,
      zoom: null,
      width: 0,
      height: 0,
      selectedNode: null,
      selectedLink: null,
      searchTerm: '',
      nodeTypeFilters: {},
      linkTypeFilters: {},
      nodeColorMap: {
        // 常见节点类型颜色映射
        'Indicator': '#1f77b4',
        'Malware': '#d62728',
        'AttackPattern': '#ff7f0e',
        'Vulnerability': '#9467bd',
        'Tool': '#2ca02c',
        'ThreatActor': '#8c564b',
        'Campaign': '#e377c2',
        'Context': '#7f7f7f',
        
        // 增加更多类型颜色映射
        'Entity': '#17becf',
        'Person': '#9c27b0',
        'Organization': '#3f51b5',
        'Location': '#009688',
        'Event': '#ff5722',
        'Object': '#795548',
        'Concept': '#607d8b',
        'Document': '#ff9800',
        'Product': '#cddc39',
        'Unknown': '#aaaaaa'
      }
    };
  },
  mounted() {
    this.initGraph();
    this.fetchGraphData();
    window.addEventListener('resize', this.resizeGraph);
  },
  beforeUnmount() {
    window.removeEventListener('resize', this.resizeGraph);
    if (this.simulation) {
      this.simulation.stop();
    }
  },
  watch: {
    apiEndpoint(newEndpoint, oldEndpoint) {
      if (newEndpoint !== oldEndpoint) {
        console.log(`API端点已更改: ${oldEndpoint} -> ${newEndpoint}`);
        // 重新获取数据
        this.loading = true;
        this.fetchGraphData();
      }
    }
  },
  methods: {
    async fetchGraphData() {
      try {
        this.loading = true;
        this.error = null;
        
        let url = '';
        
        if (this.apiEndpoint === 'graph') {
          url = `${this.apiUrl}/graph/${this.searchQuery || 'all'}`;
        } else if (this.apiEndpoint === 'entity-graph') {
          url = `${this.apiUrl}/entity-graph`;
        } else if (this.apiEndpoint === 'test-graph') {
          url = `${this.apiUrl}/test-graph`;
        }
        
        console.log('Fetching graph data from:', url);
        
        let headers = {};
        if (this.token) {
          headers = {
            'Authorization': `Bearer ${this.token}`
          };
        }
        
        const response = await fetch(url, {
          method: 'GET',
          headers: headers
        });

        if (!response.ok) {
          throw new Error(`API error: ${response.status}`);
        }

        const data = await response.json();
        console.log('Graph data received:', data);
        
        if (data.nodes && data.links) {
          // 预处理节点和链接数据
          this.nodes = data.nodes.map(node => ({
            ...node,
            id: node.id || node.nodeId || `node-${Math.random().toString(36).substr(2, 9)}`,
            group: node.labels?.[0] || 'Unknown'
          }));
          
          this.links = data.links.map(link => ({
            ...link,
            id: link.id || `link-${Math.random().toString(36).substr(2, 9)}`,
            source: typeof link.source === 'object' ? link.source.id || link.source.nodeId : link.source,
            target: typeof link.target === 'object' ? link.target.id || link.target.nodeId : link.target
          }));
          
          // 记录唯一的节点类型，用于着色
          const groupSet = new Set();
          this.nodes.forEach(node => {
            if (node.group) {
              groupSet.add(node.group);
            }
          });
          this.uniqueGroups = Array.from(groupSet);

          this.initializeGraph();
        } else {
          this.error = "Invalid data format returned from API";
        }
      } catch (error) {
        console.error('Error loading graph data:', error);
        this.error = `Failed to load graph: ${error.message}`;
      } finally {
        this.loading = false;
      }
    },
    
    initFilters() {
      // Extract unique node types
      const nodeTypes = [...new Set(this.graphData.nodes.map(node => node.type))];
      this.nodeTypeFilters = nodeTypes.reduce((acc, type) => {
        acc[type] = true;
        return acc;
      }, {});
      
      // Extract unique link types
      const linkTypes = [...new Set(this.graphData.links.map(link => link.type))];
      this.linkTypeFilters = linkTypes.reduce((acc, type) => {
        acc[type] = true;
        return acc;
      }, {});
    },
    
    applyFilters() {
      // Filter nodes based on node type and search term
      const filteredNodes = this.graphData.nodes.filter(node => {
        const typeMatch = this.nodeTypeFilters[node.type] === true;
        const searchMatch = !this.searchTerm || 
          (node.name && node.name.toLowerCase().includes(this.searchTerm.toLowerCase())) ||
          (node.value && node.value.toLowerCase().includes(this.searchTerm.toLowerCase()));
        return typeMatch && searchMatch;
      });
      
      // Get IDs of filtered nodes
      const nodeIds = new Set(filteredNodes.map(node => node.id));
      
      // Filter links based on link type and if both source and target nodes are visible
      const filteredLinks = this.graphData.links.filter(link => {
        const typeMatch = this.linkTypeFilters[link.type] === true;
        const nodesExist = nodeIds.has(link.source) && nodeIds.has(link.target);
        return typeMatch && nodesExist;
      });
      
      this.filteredData = {
        nodes: filteredNodes,
        links: filteredLinks
      };
      
      this.updateGraph();
    },
    
    initGraph() {
      const container = this.$refs.svg.parentElement;
      this.width = container.clientWidth;
      this.height = container.clientHeight;
      
      this.svg = d3.select(this.$refs.svg)
        .attr('width', this.width)
        .attr('height', this.height);
      
      // Clear existing content
      this.svg.selectAll('*').remove();
      
      // Initialize zoom behavior
      this.zoom = d3.zoom()
        .scaleExtent([0.1, 4])
        .on('zoom', (event) => {
          this.g.attr('transform', event.transform);
        });
      
      this.svg.call(this.zoom);
      
      // Create the main group element
      this.g = this.svg.append('g');
      
      // Markers for arrowheads
      this.svg.append('defs').append('marker')
        .attr('id', 'arrowhead')
        .attr('viewBox', '-0 -5 10 10')
        .attr('refX', 15)
        .attr('refY', 0)
        .attr('orient', 'auto')
        .attr('markerWidth', 8)
        .attr('markerHeight', 8)
        .attr('xoverflow', 'visible')
        .append('svg:path')
        .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
        .attr('fill', '#999')
        .style('stroke', 'none');
    },
    
    updateGraph() {
      if (!this.g) return;
      
      console.log("开始更新图谱视图...");
      console.log(`渲染 ${this.filteredData.nodes.length} 个节点和 ${this.filteredData.links.length} 个链接`);
      
      if (this.filteredData.nodes.length === 0) {
        console.warn("没有节点可渲染，请检查过滤条件或数据源");
        // 在图表中显示无数据提示
        this.g.selectAll('*').remove();
        this.g.append('text')
          .attr('x', this.width / 2)
          .attr('y', this.height / 2)
          .attr('text-anchor', 'middle')
          .text('没有可显示的图谱数据')
          .attr('fill', '#666')
          .attr('font-size', '20px');
        return;
      }
      
      // 清除现有图谱
      this.g.selectAll('*').remove();
      
      try {
        // 确保节点ID是字符串类型
        const nodes = this.filteredData.nodes.map(node => ({
          ...node,
          id: String(node.id)
        }));
        
        // 创建节点ID到节点对象的映射
        const nodeById = {};
        nodes.forEach(node => {
          nodeById[node.id] = node;
        });
        
        // 处理链接数据
        const links = this.filteredData.links.map(link => {
          // 确保source和target是字符串
          const sourceId = typeof link.source === 'object' ? link.source.id : String(link.source);
          const targetId = typeof link.target === 'object' ? link.target.id : String(link.target);
          
          return {
            ...link,
            source: sourceId, 
            target: targetId
          };
        }).filter(link => {
          // 过滤无效链接
          return nodeById[link.source] && nodeById[link.target];
        });
        
        console.log(`预处理后: ${nodes.length}个节点, ${links.length}个链接`);
        
        // 创建链接
        const link = this.g.append('g')
          .attr('class', 'links')
          .selectAll('line')
          .data(links)
          .enter()
          .append('line')
          .attr('stroke-width', d => Math.max(1, d.confidence ? d.confidence * 3 : 1))
          .attr('stroke', '#999')
          .attr('marker-end', 'url(#arrowhead)')
          .on('click', (event, d) => this.selectLink(d));
        
        // 创建节点
        const node = this.g.append('g')
          .attr('class', 'nodes')
          .selectAll('circle')
          .data(nodes)
          .enter()
          .append('circle')
          .attr('r', d => 6 + (d.size || 0))
          .attr('fill', d => this.getNodeColor(d))
          .attr('stroke', '#fff')
          .attr('stroke-width', 1.5)
          .on('click', (event, d) => this.selectNode(d))
          .call(this.drag());
        
        // 添加节点标签
        const label = this.g.append('g')
          .attr('class', 'labels')
          .selectAll('text')
          .data(nodes)
          .enter()
          .append('text')
          .text(d => d.name || d.id)
          .attr('font-size', 10)
          .attr('dx', 12)
          .attr('dy', 4);
        
        // 设置力导向模拟
        if (this.simulation) {
          this.simulation.stop();
        }
        
        const simulation = d3.forceSimulation(nodes)
          .force('link', d3.forceLink(links).id(d => d.id).distance(100))
          .force('charge', d3.forceManyBody().strength(-100))
          .force('center', d3.forceCenter(this.width / 2, this.height / 2))
          .force('collision', d3.forceCollide().radius(20));
        
        // 添加tick事件处理
        simulation.on('tick', () => {
          // 防止节点移出视图范围
          nodes.forEach(d => {
            d.x = Math.max(30, Math.min(this.width - 30, d.x || this.width/2));
            d.y = Math.max(30, Math.min(this.height - 30, d.y || this.height/2));
          });
          
          link
            .attr('x1', d => {
              const source = typeof d.source === 'object' ? d.source : nodeById[d.source];
              return source ? source.x : 0;
            })
            .attr('y1', d => {
              const source = typeof d.source === 'object' ? d.source : nodeById[d.source];
              return source ? source.y : 0;
            })
            .attr('x2', d => {
              const target = typeof d.target === 'object' ? d.target : nodeById[d.target];
              return target ? target.x : 0;
            })
            .attr('y2', d => {
              const target = typeof d.target === 'object' ? d.target : nodeById[d.target];
              return target ? target.y : 0;
            });
          
          node
            .attr('cx', d => d.x)
            .attr('cy', d => d.y);
          
          label
            .attr('x', d => d.x)
            .attr('y', d => d.y);
        })
        .on('end', () => {
          console.log("力导向模拟完成");
        });
        
        // 保存模拟引用
        this.simulation = simulation;
        
        // 控制模拟热度
        this.simulation.alpha(0.8).restart();
        
        console.log("图谱视图更新完成");
      } catch (error) {
        console.error("渲染图谱时发生错误:", error);
        this.g.selectAll('*').remove();
        this.g.append('text')
          .attr('x', this.width / 2)
          .attr('y', this.height / 2)
          .attr('text-anchor', 'middle')
          .text('图谱渲染错误: ' + error.message)
          .attr('fill', 'red')
          .attr('font-size', '16px');
      }
      
      // 初始居中
      this.centerGraph();
    },
    
    drag() {
      return d3.drag()
        .on('start', (event, d) => {
          if (!event.active) this.simulation.alphaTarget(0.3).restart();
          d.fx = d.x;
          d.fy = d.y;
        })
        .on('drag', (event, d) => {
          d.fx = event.x;
          d.fy = event.y;
        })
        .on('end', (event, d) => {
          if (!event.active) this.simulation.alphaTarget(0);
          d.fx = null;
          d.fy = null;
        });
    },
    
    getNodeColor(node) {
      return this.nodeColorMap[node.type] || '#ccc';
    },
    
    selectNode(node) {
      this.selectedNode = node;
      this.selectedLink = null;
    },
    
    selectLink(link) {
      this.selectedLink = link;
      this.selectedNode = null;
    },
    
    closeDetails() {
      this.selectedNode = null;
      this.selectedLink = null;
    },
    
    getNodeNameById(id) {
      const node = this.graphData.nodes.find(n => n.id === id);
      return node ? node.name || id : id;
    },
    
    formatDate(dateString) {
      if (!dateString) return '';
      try {
        const date = new Date(dateString);
        return date.toLocaleString();
      } catch (e) {
        return dateString;
      }
    },
    
    resizeGraph() {
      const container = this.$refs.svg.parentElement;
      this.width = container.clientWidth;
      this.height = container.clientHeight;
      
      this.svg
        .attr('width', this.width)
        .attr('height', this.height);
      
      if (this.simulation) {
        this.simulation.force('center', d3.forceCenter(this.width / 2, this.height / 2));
        this.simulation.restart();
      }
    },
    
    resetZoom() {
      this.svg.transition().duration(750).call(
        this.zoom.transform,
        d3.zoomIdentity
      );
    },
    
    centerGraph() {
      this.resetZoom();
      
      if (this.simulation) {
        this.simulation.force('center', d3.forceCenter(this.width / 2, this.height / 2));
        this.simulation.alpha(0.3).restart();
      }
    },
    
    zoomIn() {
      this.svg.transition().call(this.zoom.scaleBy, 1.2);
    },
    
    zoomOut() {
      this.svg.transition().call(this.zoom.scaleBy, 0.8);
    },
    
    downloadSVG() {
      const svgEl = this.$refs.svg;
      const svgData = new XMLSerializer().serializeToString(svgEl);
      const blob = new Blob([svgData], { type: 'image/svg+xml' });
      const url = URL.createObjectURL(blob);
      
      const link = document.createElement('a');
      link.href = url;
      link.download = `knowledge_graph_${new Date().toISOString().split('T')[0]}.svg`;
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      URL.revokeObjectURL(url);
    }
  }
};
</script>

<style scoped>
.knowledge-graph-container {
  display: flex;
  height: 100%;
  width: 100%;
}

.control-panel {
  width: 300px;
  padding: 15px;
  border-right: 1px solid #ddd;
  height: 100%;
  overflow-y: auto;
  background-color: #f8f9fa;
}

.graph-container {
  flex: 1;
  position: relative;
  overflow: hidden;
}

.filters {
  margin-bottom: 20px;
}

.filter-group {
  margin-bottom: 15px;
}

.filter-group h3 {
  margin-top: 0;
  margin-bottom: 10px;
  font-size: 16px;
  color: #333;
}

.filter-item {
  display: flex;
  align-items: center;
  margin-bottom: 5px;
}

.filter-item label {
  margin-left: 5px;
  padding: 2px 6px;
  border-radius: 3px;
  color: #fff;
}

.actions {
  margin-bottom: 20px;
}

.action-button {
  margin-right: 8px;
  margin-bottom: 8px;
  padding: 6px 12px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.action-button:hover {
  background-color: #0056b3;
}

.node-details, .link-details {
  background-color: white;
  border: 1px solid #ddd;
  border-radius: 5px;
  padding: 15px;
  margin-top: 15px;
}

.detail-row {
  margin-bottom: 8px;
}

.close-button {
  margin-top: 10px;
  padding: 4px 8px;
  background-color: #6c757d;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.legend {
  position: absolute;
  top: 10px;
  right: 10px;
  background-color: rgba(255, 255, 255, 0.8);
  padding: 10px;
  border-radius: 5px;
  border: 1px solid #ddd;
}

.legend-item {
  display: flex;
  align-items: center;
  margin-bottom: 5px;
}

.legend-color {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  margin-right: 5px;
}

.zoom-controls {
  position: absolute;
  bottom: 20px;
  right: 20px;
  display: flex;
  flex-direction: column;
}

.zoom-button {
  width: 30px;
  height: 30px;
  margin-bottom: 5px;
  border-radius: 15px;
  background-color: white;
  border: 1px solid #ccc;
  cursor: pointer;
  font-size: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.loading-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(255, 255, 255, 0.7);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.spinner {
  width: 40px;
  height: 40px;
  margin-bottom: 10px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
</style> 